/*
 * Copyright 2014 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.util.concurrent;

import io.netty.util.internal.InternalThreadLocalMap;
import io.netty.util.internal.PlatformDependent;

import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;

/**
 * A special variant of {@link ThreadLocal} that yields higher access
 * performance when accessed from a {@link FastThreadLocalThread}.
 * <p>
 * Internally, a {@link FastThreadLocal} uses a constant index in an array,
 * instead of using hash code and hash table, to look for a variable. Although
 * seemingly very subtle, it yields slight performance advantage over using a
 * hash table, and it is useful when accessed frequently.
 * </p>
 * <p>
 * To take advantage of this thread-local variable, your thread must be a
 * {@link FastThreadLocalThread} or its subtype. By default, all threads created
 * by {@link DefaultThreadFactory} are {@link FastThreadLocalThread} due to this
 * reason.
 * </p>
 * <p>
 * Note that the fast path is only possible on threads that extend
 * {@link FastThreadLocalThread}, because it requires a special field to store
 * the necessary state. An access by any other kind of thread falls back to a
 * regular {@link ThreadLocal}.
 * </p>
 *
 * @param <V> the type of the thread-local variable
 * @see ThreadLocal
 */
public class FastThreadLocal<V>
{

    private static final int variablesToRemoveIndex = InternalThreadLocalMap
            .nextVariableIndex();

    /**
     * Removes all {@link FastThreadLocal} variables bound to the current
     * thread. This operation is useful when you are in a container environment,
     * and you don't want to leave the thread local variables in the threads you
     * do not manage.
     */
    public static void removeAll()
    {
        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap
                .getIfSet();
        if (threadLocalMap == null)
        {
            return;
        }

        try
        {
            Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
            if (v != null && v != InternalThreadLocalMap.UNSET)
            {
                @SuppressWarnings("unchecked")
                Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;
                FastThreadLocal<?>[] variablesToRemoveArray = variablesToRemove
                        .toArray(new FastThreadLocal[variablesToRemove.size()]);
                for (FastThreadLocal<?> tlv : variablesToRemoveArray)
                {
                    tlv.remove(threadLocalMap);
                }
            }
        }
        finally
        {
            InternalThreadLocalMap.remove();
        }
    }

    /**
     * Returns the number of thread local variables bound to the current thread.
     */
    public static int size()
    {
        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap
                .getIfSet();
        if (threadLocalMap == null)
        {
            return 0;
        }
        else
        {
            return threadLocalMap.size();
        }
    }

    /**
     * Destroys the data structure that keeps all {@link FastThreadLocal}
     * variables accessed from non-{@link FastThreadLocalThread}s. This
     * operation is useful when you are in a container environment, and you do
     * not want to leave the thread local variables in the threads you do not
     * manage. Call this method when your application is being unloaded from the
     * container.
     */
    public static void destroy()
    {
        InternalThreadLocalMap.destroy();
    }

    @SuppressWarnings("unchecked")
    private static void addToVariablesToRemove(
            InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable)
    {
        Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
        Set<FastThreadLocal<?>> variablesToRemove;
        if (v == InternalThreadLocalMap.UNSET || v == null)
        {
            variablesToRemove = Collections.newSetFromMap(
                    new IdentityHashMap<FastThreadLocal<?>, Boolean>());
            threadLocalMap.setIndexedVariable(variablesToRemoveIndex,
                    variablesToRemove);
        }
        else
        {
            variablesToRemove = (Set<FastThreadLocal<?>>) v;
        }

        variablesToRemove.add(variable);
    }

    private static void removeFromVariablesToRemove(
            InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable)
    {

        Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);

        if (v == InternalThreadLocalMap.UNSET || v == null)
        {
            return;
        }

        @SuppressWarnings("unchecked")
        Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;
        variablesToRemove.remove(variable);
    }

    private final int index;

    public FastThreadLocal()
    {
        index = InternalThreadLocalMap.nextVariableIndex();
    }

    /**
     * Returns the current value for the current thread
     */
    public final V get()
    {
        return get(InternalThreadLocalMap.get());
    }

    /**
     * Returns the current value for the specified thread local map. The
     * specified thread local map must be for the current thread.
     */
    @SuppressWarnings("unchecked")
    public final V get(InternalThreadLocalMap threadLocalMap)
    {
        Object v = threadLocalMap.indexedVariable(index);
        if (v != InternalThreadLocalMap.UNSET)
        {
            return (V) v;
        }

        return initialize(threadLocalMap);
    }

    private V initialize(InternalThreadLocalMap threadLocalMap)
    {
        V v = null;
        try
        {
            v = initialValue();
        }
        catch (Exception e)
        {
            PlatformDependent.throwException(e);
        }

        threadLocalMap.setIndexedVariable(index, v);
        addToVariablesToRemove(threadLocalMap, this);
        return v;
    }

    /**
     * Set the value for the current thread.
     */
    public final void set(V value)
    {
        if (value != InternalThreadLocalMap.UNSET)
        {
            set(InternalThreadLocalMap.get(), value);
        }
        else
        {
            remove();
        }
    }

    /**
     * Set the value for the specified thread local map. The specified thread
     * local map must be for the current thread.
     */
    public final void set(InternalThreadLocalMap threadLocalMap, V value)
    {
        if (value != InternalThreadLocalMap.UNSET)
        {
            if (threadLocalMap.setIndexedVariable(index, value))
            {
                addToVariablesToRemove(threadLocalMap, this);
            }
        }
        else
        {
            remove(threadLocalMap);
        }
    }

    /**
     * Returns {@code true} if and only if this thread-local variable is set.
     */
    public final boolean isSet()
    {
        return isSet(InternalThreadLocalMap.getIfSet());
    }

    /**
     * Returns {@code true} if and only if this thread-local variable is set.
     * The specified thread local map must be for the current thread.
     */
    public final boolean isSet(InternalThreadLocalMap threadLocalMap)
    {
        return threadLocalMap != null
                && threadLocalMap.isIndexedVariableSet(index);
    }

    /**
     * Sets the value to uninitialized; a proceeding call to get() will trigger
     * a call to initialValue().
     */
    public final void remove()
    {
        remove(InternalThreadLocalMap.getIfSet());
    }

    /**
     * Sets the value to uninitialized for the specified thread local map; a
     * proceeding call to get() will trigger a call to initialValue(). The
     * specified thread local map must be for the current thread.
     */
    @SuppressWarnings("unchecked")
    public final void remove(InternalThreadLocalMap threadLocalMap)
    {
        if (threadLocalMap == null)
        {
            return;
        }

        Object v = threadLocalMap.removeIndexedVariable(index);
        removeFromVariablesToRemove(threadLocalMap, this);

        if (v != InternalThreadLocalMap.UNSET)
        {
            try
            {
                onRemoval((V) v);
            }
            catch (Exception e)
            {
                PlatformDependent.throwException(e);
            }
        }
    }

    /**
     * Returns the initial value for this thread-local variable.
     */
    protected V initialValue() throws Exception
    {
        return null;
    }

    /**
     * Invoked when this thread local variable is removed by {@link #remove()}.
     */
    protected void onRemoval(@SuppressWarnings("UnusedParameters") V value)
            throws Exception
    {
    }
}
